package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"html/template"
	"io"
	"log"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strings"

	rice "github.com/GeertJohan/go.rice"
)

// Configure 启动配置
type Configure struct {
	Title  string
	Prefix string
	Root   string
	Port   int
}

// ParseConfigure 解析命令行的配置
func ParseConfigure() *Configure {
	conf := &Configure{}

	flag.StringVar(&conf.Title, "title", "File Server", "Title of this site.")
	flag.StringVar(&conf.Prefix, "prefix", "", "Set the prefix URL when using behide NGINX")
	flag.StringVar(&conf.Root, "root", "./", "Set the path for shared files")
	flag.IntVar(&conf.Port, "port", 8088, "Listen port")
	flag.Parse()

	conf.Root = strings.TrimSuffix(conf.Root, "/")
	conf.Prefix = strings.TrimSuffix(conf.Prefix, "/")

	return conf
}

// FormatSize 生成可读的文件大小
func FormatSize(size int64) string {
	if size < 1024 {
		return fmt.Sprintf("%d B", size)
	}

	if size < 1024*1024 {
		return fmt.Sprintf("%.03f KB", float32(size)/1024)
	}

	if size < 1024*1024*1024 {
		return fmt.Sprintf("%.03f MB", float32(size)/(1024*1024))
	}

	return fmt.Sprintf("%.03f GB", float32(size)/(1024*1024*1024))
}

// FindFileIcon 根据后缀给出文件的图标
func FindFileIcon(name string) string {
	ext := filepath.Ext(name)
	switch ext {
	case ".txt", ".json":
		return "file-text-o"
	case ".doc", ".docx":
		return "file-word-o"
	case ".ppt", ".pptx":
		return "file-powerpoint-o"
	case ".xsl", ".xslx":
		return "file-excel-o"
	case ".db", ".sql", ".pdb":
		return "database"
	case ".zip", ".rar", ".7z", ".bin", ".lzma", ".gz", ".tar":
		return "file-archive-o"
	case ".mp3", ".wav", ".ogg", ".snd":
		return "file-audio-o"
	case ".png", ".jpg", ".jpeg", ".bmp", ".tga", ".psd", ".ico":
		return "file-image-o"
	case ".pdf":
		return "file-pdf-o"
	case ".mp4", ".avi", ".rmvb", ".asf", ".mov", ".wmv", ".3gp", ".rm":
		return "file-video-o"
	case ".h", ".hpp", ".c", ".cpp", ".cc", ".cxx", ".cs", ".go", ".java", ".ts", ".tsx", ".py":
		return "file-code-o"
	case ".html", ".htm":
		return "html5"
	case ".css":
		return "css3"
	case ".js":
		return "jsfiddle"
	case ".sh":
		return "linux"
	case ".apk":
		return "android"
	case ".ipa":
		return "apple"
	case ".exe", ".dll":
		return "windows"
	case ".md":
		return "book"
	default:
		return "file-o"
	}
}

// Service HTTP文件服务
type Service struct {
	conf   *Configure
	assets *rice.HTTPBox
	page   *template.Template
}

// Start 启动服务
func (s *Service) Start() {
	s.conf = ParseConfigure()
	s.assets = rice.MustFindBox("assets").HTTPBox()
	s.page, _ = template.New("MAIN").Parse(s.assets.MustString("index.html"))

	err := http.ListenAndServe(fmt.Sprintf(":%d", s.conf.Port), s)
	if err != nil {
		log.Fatalf("Service exit with error : %v\n", err)
	}
}

// ServeHTTP 实现http.Handler
func (s *Service) ServeHTTP(rsp http.ResponseWriter, req *http.Request) {
	req.URL.Path = strings.TrimPrefix(req.URL.Path, s.conf.Prefix)
	log.Printf("%5s %s\n", req.Method, req.URL.Path)

	switch req.Method {
	case "GET":
		s.serveContent(rsp, req)
	case "POST":
		s.serveAPI(rsp, req)
	default:
		rsp.WriteHeader(http.StatusMethodNotAllowed)
		rsp.Header().Set("Content-Type", "text/html")
		rsp.Write([]byte(`
			<div style="margin-top: 10%; text-align: center; font-weight: bolder; font-size: 48px; color: lightgray">
				:( 405 METHOD NOT ALLOWED
				<br><hr>
			</div>
		`))
	}
}

func (s *Service) serveContent(rsp http.ResponseWriter, req *http.Request) {
	path := req.URL.Path

	if strings.HasPrefix(path, "/-/assets/") {
		req.URL.Path = strings.TrimPrefix(path, "/-/assets")
		http.FileServer(s.assets).ServeHTTP(rsp, req)
		return
	}

	file, err := os.Open(s.conf.Root + path)
	if err != nil {
		s.page.Execute(rsp, &map[string]interface{}{
			"Title":   s.conf.Title,
			"Prefix":  s.conf.Prefix,
			"Path":    path,
			"IsError": true,
		})
		return
	}

	defer file.Close()

	info, err := file.Stat()
	if err != nil {
		s.page.Execute(rsp, &map[string]interface{}{
			"Title":   s.conf.Title,
			"Prefix":  s.conf.Prefix,
			"Path":    path,
			"IsError": true,
		})
		return
	}

	if info.IsDir() {
		rsp.Header().Set("Content-Type", "text/html")

		type Item struct {
			Name    string
			Size    string
			IsDir   bool
			ModTime string
			Path    string
			Icon    string
		}

		readme := ""
		dirs, err := file.Readdir(-1)
		if err != nil {
			dirs = []os.FileInfo{}
		}

		items := []*Item{}
		for _, dir := range dirs {
			item := &Item{
				Name:    dir.Name(),
				IsDir:   dir.IsDir(),
				ModTime: dir.ModTime().Format("2006-01-02 15:04:05"),
				Path:    s.conf.Prefix + strings.TrimSuffix(path, "/") + "/" + dir.Name(),
			}

			if item.IsDir {
				item.Size = ""
				item.Icon = "folder"
			} else {
				if strings.ToLower(item.Name) == "readme.md" {
					readme = item.Path
				}

				item.Size = FormatSize(dir.Size())
				item.Icon = FindFileIcon(item.Name)
			}

			items = append(items, item)
		}

		sort.Slice(items, func(i, j int) bool {
			if items[i].IsDir == items[j].IsDir {
				return strings.Compare(items[i].Name, items[j].Name) < 0
			}

			return items[i].IsDir
		})

		s.page.Execute(rsp, &map[string]interface{}{
			"Title":    s.conf.Title,
			"Prefix":   s.conf.Prefix,
			"Path":     path,
			"Contents": items,
			"ReadMe":   readme,
		})

		return
	}

	rsp.Header().Set("Content-Disposition", "attachment;filename="+info.Name())
	http.ServeContent(rsp, req, info.Name(), info.ModTime(), file)
}

func (s *Service) serveAPI(rsp http.ResponseWriter, req *http.Request) {
	type Response struct {
		IsOK   bool   `json:"ok"`
		ErrMsg string `json:"errMsg"`
	}

	status := http.StatusOK
	ret := &Response{IsOK: false, ErrMsg: "UNKNOWN"}

	switch req.URL.Path {
	case "/-/api/folder":
		path := req.FormValue("path")
		name := req.FormValue("name")
		check := path + name

		if check == s.conf.Prefix || check == "/-" {
			ret.ErrMsg = "保留字段，请使用其他名字！"
			break
		}

		if name == "" || strings.ContainsAny(name, "./\\?!~+") {
			ret.ErrMsg = "非法目录名！"
			break
		}

		err := os.MkdirAll(s.conf.Root+path+"/"+name, 777)
		if err != nil {
			ret.ErrMsg = "创建目录失败：" + err.Error()
			break
		}

		ret.IsOK = true
	case "/-/api/upload":
		if req.MultipartForm == nil {
			if err := req.ParseMultipartForm(64 << 20); err != nil {
				ret.ErrMsg = "解析请求失败：" + err.Error()
				break
			}
		}

		path := req.FormValue("path")
		parts := req.MultipartForm.File
		failed := false
		for _, part := range parts {
			for _, fh := range part {
				check := path + fh.Filename
				if check == s.conf.Prefix || check == "/-" {
					ret.ErrMsg = "文件名与保留字冲突！"
					failed = true
					break
				}

				if err := s.save(fh, check); err != nil {
					ret.ErrMsg = "保存文件失败：" + err.Error()
					failed = true
					break
				}
			}

			if failed {
				break
			}
		}

		ret.IsOK = !failed
	default:
		status = http.StatusBadRequest
		ret.ErrMsg = "非法的请求!"
	}

	data, _ := json.Marshal(ret)
	rsp.WriteHeader(status)
	rsp.Header().Set("Content-Type", "application/json")
	rsp.Write(data)
}

func (s *Service) save(fh *multipart.FileHeader, dir string) error {
	reader, err := fh.Open()
	if err != nil {
		return err
	}

	defer reader.Close()

	writer, err := os.OpenFile(s.conf.Root+dir, os.O_WRONLY|os.O_CREATE, 0777)
	if err != nil {
		return err
	}

	defer writer.Close()

	_, err = io.Copy(writer, reader)
	return err
}

func main() {
	(&Service{}).Start()
}
